﻿/*
BF3 RCON Simulator simulates the RCON interface of a BF3 server.
Copyright (C) 2011 Timi

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

You can contact us at http://bf3sim.codeplex.com/.
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Dynamic;

namespace BF3RconSimulator
{
    //will handle vars.* thru RconSimulator.Vars["vars.*"] and SetVar
    class VarsData
    {
        //hashsets used because contains is O(1)
        //since they're unique characters, a hashset is good
        static readonly HashSet<char> ValidPasswordCharacters = new HashSet<char>
            ("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");
        //all of these can be evaluated in the same way
        static readonly HashSet<string> BooleanVars = new HashSet<string>(StringComparer.OrdinalIgnoreCase) 
        {
            "autoBalance", "friendlyFire", "killCam", "miniMap", "hud", "crossHair", "3dSpotting", "miniMapSpotting",
            "nameTag", "3pCam", "regenerateHealth", "vehicleSpawnAllowed", "onlySquadLeaderSpawn"
        };
        //for numeric vars that have a specific range. for gamemodecounter, this is what i tested it to
        //clamped and not covered here: roundStartPlayerCount roundRestartPlayerCount
        static readonly Dictionary<string, NumericRange> NumericVars = new Dictionary<string, NumericRange>(StringComparer.OrdinalIgnoreCase)
        {
            {"maxPlayers", new UIntegerRange(8, 32)}, {"teamKillCountForKick", new UIntegerRange(0, uint.MaxValue)},
            {"teamKillValueForKick", new NumericRange<float>(0.0f, float.MaxValue)}, {"teamKillValueIncrease", new NumericRange<float>(0.0f, float.MaxValue)},
            {"teamKillValueDecreasePerSecond", new NumericRange<float>(0.0f, float.MaxValue)}, {"teamKillKickForBan", new UIntegerRange(0, uint.MaxValue)}, 
            {"idleTimeout", new UIntegerRange(0, uint.MaxValue)}, {"idleBanRounds", new UIntegerRange(0, uint.MaxValue)},
            {"vehicleSpawnDelay", new NumericRange<int>(0, 100)},
            {"playerRespawnTime", new NumericRange<int>(0, 100)}, {"playerManDownTime", new NumericRange<int>(0, 100)},
            {"bulletDamage", new NumericRange<int>(0, 100)}, {"gameModeCounter", new UIntegerRange(0, uint.MaxValue)} //gamemodecounter actually has weird behavior, cant reproduce
        };
        //TODO: check soldierhealth clamp
        //TODO: check other pdf

        Dictionary<string, string> VarsDictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        RconSimulator Parent;

        internal VarsData(RconSimulator parent)
        {
            Parent = parent;
        }

        //created because this will be an often-used property
        internal bool Ranked
        {
            get
            {
                string result;
                return VarsDictionary.TryGetValue("vars.ranked", out result) ? result.CaseInsensitiveEquals("True") : false;
            }
        }

        //only get because set needs to return a value
        internal Response GetVar(string var)
        {
            //for ease, store value in var if success
            if (VarsDictionary.TryGetValue(var, out var)) return Response.Success("OK", var);
            else return Response.Fail();
        }

        internal Response SetVar(string var, string value)
        {
            string varPart = var.Substring(5).ToLowerInvariant();

            //anything requiring special validation or that effects the simulator is handled here
            switch (varPart) //get the stuff after vars. just to shorten the case labels
            {
                case "gamepassword":
                    //if started and ranked, cant be set
                    if (Parent.Server.Listening && Ranked)
                        return Response.Fail("InvalidConfig");

                    foreach (char c in value)
                    {
                        if (ValidPasswordCharacters.Contains(c)) //value contains invalid character
                        {
                            return Response.Fail("InvalidPassword");
                        }
                    }

                    goto success;
                case "roundstartplayercount":
                    if (value == "0")
                        return Response.Fail("InvalidValue");

                    int valueInt; //this one is int, not uint
                    try
                    {
                        valueInt = int.Parse(value);
                    }
                    catch (OverflowException)
                    {
                        valueInt = -1;
                    }
                    catch (FormatException)
                    {
                        //see if it's a float instead
                        double valueDouble;
                        try
                        {
                            if (double.TryParse(value, out valueDouble))
                                valueInt = Convert.ToInt32(valueDouble);
                        }
                        catch (Exception)
                        {
                            //TODO: for numeric range, check to see if double when int
                            throw;
                        }
                        return Response.Fail("InvalidValue");
                    }                   

                    if (valueInt < 0) value = "-1"; //values < 0 go to -1 for some reason


                    goto success;
                case "roundrestartplayercount":
                    //-1, 0, rest
                    goto success;
                case "soldierHealth":

                    goto success;
                case "servername":
                    //servername TooLongName is 63
                    //TODO: ensure vars.(*) is actual var
                    goto success;
                case "ranked":
                    //TODO: block when started, and impossible to go from false to true
                    if (Parent.Server.Listening || //is currently started
                        (VarsDictionary.ContainsKey("vars.ranked") && !Ranked)) //is not started; ranked has been set and
                        return Response.Fail("CommandIsReadOnly");
                    
                    goto success;
            }

            //do other checking like boolean vars here now that special checking is done
            if (BooleanVars.Contains(varPart))
            {
                //TODO: confirm this is actual behavior
                if (!value.CaseInsensitiveEquals("True", "False")) return Response.Fail("InvalidArguments");

                //store the data in lowercase, as is done in server
                value = value.ToLowerInvariant();
                goto success;
            }

            //numeric
            NumericRange targetRange;
            if (NumericVars.TryGetValue(varPart, out targetRange))
            {
                //for some reason, invalid inputs set to 0 and respond OK
                if (!targetRange.IsInRange(value))
                    value = targetRange.DefaultValue().ToString();

                goto success;
            }

            //if it gets this far, the var isnt found
            return Response.Fail();

            //used a goto because it's better to just have one return Response.Success();
            success:
            VarsDictionary[var] = value;
            return Response.Success();
        }
    }
}
